Conversation
ID5 basic module See merge request id5-sync/prebid-server-java!1
...ain/java/org/prebid/server/hooks/modules/id5/userid/config/Id5UserIdModuleConfiguration.java
Outdated
Show resolved
Hide resolved
...ain/java/org/prebid/server/hooks/modules/id5/userid/config/Id5UserIdModuleConfiguration.java
Show resolved
Hide resolved
...ain/java/org/prebid/server/hooks/modules/id5/userid/config/Id5UserIdModuleConfiguration.java
Outdated
Show resolved
Hide resolved
...ain/java/org/prebid/server/hooks/modules/id5/userid/config/Id5UserIdModuleConfiguration.java
Outdated
Show resolved
Hide resolved
...rc/main/java/org/prebid/server/hooks/modules/id5/userid/v1/config/Id5IdModuleProperties.java
Outdated
Show resolved
Hide resolved
...5-user-id/src/main/java/org/prebid/server/hooks/modules/id5/userid/v1/fetch/FetchClient.java
Outdated
Show resolved
Hide resolved
...er-id/src/main/java/org/prebid/server/hooks/modules/id5/userid/v1/fetch/HttpFetchClient.java
Outdated
Show resolved
Hide resolved
...er-id/src/main/java/org/prebid/server/hooks/modules/id5/userid/v1/fetch/HttpFetchClient.java
Outdated
Show resolved
Hide resolved
...er-id/src/main/java/org/prebid/server/hooks/modules/id5/userid/v1/fetch/HttpFetchClient.java
Outdated
Show resolved
Hide resolved
...er-id/src/main/java/org/prebid/server/hooks/modules/id5/userid/v1/fetch/HttpFetchClient.java
Show resolved
Hide resolved
|
@CTMBNara Thank you for the review. I've updated the code. Could you please take another look? |
| @PropertySource( | ||
| value = "classpath:/module-config/id5-user-id.yaml", | ||
| factory = YamlPropertySourceFactory.class) |
There was a problem hiding this comment.
Remove this. Use --spring.config.additional-location or something similar to it
There was a problem hiding this comment.
It seems to me that this is actually the standard approach, as we already have several similar examples in the codebase implemented in the same way. Each bidder follows this pattern, and other modules, like i.e. https://github.com/prebid/prebid-server-java/blob/a35ff7eafb5bee500866a06f3186cb4d4c60c1f8/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/config/ConfiantAdQualityModuleConfiguration.java#L27C1-L27C17
The goal here is to provide default values in the simplest possible way. Defaults are not something users are usually expected to change, so they should work out of the box without any additional configuration.
The suggested change would require adding specific JVM runtime parameters for the defaults to be loaded. This seems to go against the idea of defaults. With the current solution, the behavior is enabled only when the module is on the classpath, which means someone intentionally included it.
There was a problem hiding this comment.
@pkowalski-id5 Bidders is а core feature of prebid-server, while modules are optional. We don't stick with the idea to require separate file for modules configuration. Each module may have config in main .yaml, separate, etc.
There was a problem hiding this comment.
As for the example you mentioned above: historically, this module has passed a "very light test", and we do not recommend it as an example.
| factory = YamlPropertySourceFactory.class) | ||
| public class Id5UserIdModuleConfiguration { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(Id5UserIdModuleConfiguration.class); |
There was a problem hiding this comment.
LOG -> logger (it's excluded in checkstyle check, so you can use lowercase name for static final)
| @Slf4j | ||
| public class HttpFetchClient implements FetchClient { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(HttpFetchClient.class); |
| VersionInfo versionInfo, | ||
| Id5IdModuleProperties id5IdModuleProperties, | ||
| UserFpdActivityMask userFpdActivityMask) { | ||
| this.fetchUrl = Objects.requireNonNull(endpoint); |
There was a problem hiding this comment.
Add empty line after multi-line declaration
| public Future<Id5UserId> fetch(long partnerId, | ||
| AuctionRequestPayload payload, | ||
| AuctionInvocationContext invocationContext) { | ||
| final FetchRequest fetchRequest = createFetchRequest(partnerId, payload, invocationContext); |
| final FetchRequest fetchRequest = createFetchRequest(partnerId, payload, invocationContext); | ||
| final String body = mapper.encodeToString(fetchRequest); | ||
| final MultiMap headers = HttpUtil.headers(); | ||
| final String url = String.format("%s/%s.json", fetchUrl, partnerId); |
There was a problem hiding this comment.
"%s/%s.json".formatted(fetchUrl, partnerId)
| return fetchRequestBuilder.build(); | ||
| } | ||
|
|
||
| private BidRequest getMaskedBidRequest(BidRequest bidRequest, AuctionInvocationContext invocationContext) { |
There was a problem hiding this comment.
getMaskedBidRequest -> maskBidRequest
| private FetchRequest createFetchRequest(long partnerId, | ||
| AuctionRequestPayload payload, | ||
| AuctionInvocationContext invocationContext) { |
There was a problem hiding this comment.
privacyContext and privacyContext.getPrivacy() != null can't be null at this point
private FetchRequest createFetchRequest(long partnerId,
AuctionRequestPayload payload,
AuctionInvocationContext invocationContext) {
final BidRequest bidRequest = maskBidRequest(payload.bidRequest(), invocationContext);
final Privacy privacy = invocationContext.auctionContext().getPrivacyContext().getPrivacy();
final Optional<Device> device = Optional.ofNullable(bidRequest.getDevice());
final Optional<Site> site = Optional.ofNullable(bidRequest.getSite());
return FetchRequest.builder()
.trace(invocationContext.debugEnabled())
.partnerId(partnerId)
.origin("pbs-java")
.version(versionInfo.getVersion())
.timestamp(clock.instant().toString())
.provider(id5IdModuleProperties.getProviderName())
.providerMetadata(createProviderMetadata(bidRequest))
.bundle(Optional.ofNullable(bidRequest.getApp()).map(App::getBundle).orElse(null))
.domain(site.map(Site::getDomain).orElse(null))
.maid(device.map(Device::getIfa).orElse(null))
.userAgent(device.map(Device::getUa).orElse(null))
.ref(site.map(Site::getRef).orElse(null))
.ipv4(device.map(Device::getIp).orElse(null))
.ipv6(device.map(Device::getIpv6).orElse(null))
.att(device
.map(Device::getExt)
.map(ExtDevice::getAtts)
.map(String::valueOf)
.orElse(null))
.coppa(Objects.toString(privacy.getCoppa(), null))
.usPrivacy(privacy.getCcpa().getUsPrivacy())
.gppString(privacy.getGpp())
.gppSid(toStringOrNull(privacy.getGppSid()))
.gdpr(privacy.getGdpr())
.gdprConsent(privacy.getConsentString())
.build();
}
There was a problem hiding this comment.
private static String toStringOrNull(List<Integer> list) {
return CollectionUtils.isNotEmpty(list)
? list.stream().map(String::valueOf).collect(Collectors.joining())
: null;
}
| @Override | ||
| public FilterResult shouldInvoke(AuctionRequestPayload payload, AuctionInvocationContext invocationContext) { | ||
| final boolean shouldInvoke = random.nextDouble() <= sampleRate; | ||
| return shouldInvoke ? FilterResult.accepted() : FilterResult.rejected("rejected by sampling"); |
| return biddersFilter.isValueAllowed(bidder) ? FilterResult.accepted() | ||
| : FilterResult.rejected("bidder " + bidder + " rejected by config"); |
| @JsonProperty("us_privacy") | ||
| String usPrivacy; |
There was a problem hiding this comment.
No need for @JsonProperty here. snake_case is a standard for our object mapper
There was a problem hiding this comment.
Thank you for raising this. It made me realise how tightly coupled and risky it was to rely on the shared mapper.
The global ObjectMapper uses SNAKE_CASE today, but that is an implementation detail of the host application, not a guarantee this module can rely on. What's more, several fields use names that don't follow SNAKE_CASE at all like providerMetadata or _trace. They are defined by the ID5 API schema, not by any naming convention. Removing @JsonProperty and trusting the naming strategy would silently couple the wire format of ID5 requests to the
host configuration. If that ever changes, the breakage would be invisible at compile time and potentially very hard to trace in production.
HttpFetchClient will own a private ObjectMapper configured independently of the application-wide one. Together with explicit @JsonProperty annotations, this guarantees the request is serialized according to the ID5 API schema regardless of any changes to the host configuration.
| @JsonProperty("gdpr_consent") | ||
| String gdprConsent; | ||
|
|
||
| @JsonProperty("gpp_string") | ||
| String gppString; | ||
|
|
||
| @JsonProperty("gpp_sid") | ||
| String gppSid; |
| String channel; | ||
| String channelVersion; | ||
| Id5IdModuleProperties id5ModuleConfig; | ||
| Publisher publisher; | ||
| List<String> bidders; | ||
| } | ||
|
|
||
| @Builder | ||
| @Value | ||
| public static class Publisher { | ||
| String id; | ||
| String name; | ||
| String domain; |
There was a problem hiding this comment.
Add empty lines between fields and after class name
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
||
| public record FetchResponse(Map<String, UserId> ids) implements Id5UserId { |
| private BidRequestUtils() { } | ||
|
|
||
| public static final String ID5_ID_SOURCE = "id5-sync.com"; | ||
|
|
||
| public static boolean isId5IdPresent(BidRequest bidRequest) { | ||
| return Optional.ofNullable(bidRequest.getUser()) | ||
| .map(User::getEids) | ||
| .map(eids -> eids.stream().anyMatch(eid -> ID5_ID_SOURCE.equals(eid.getSource()))) | ||
| .orElse(false); | ||
| } | ||
|
|
There was a problem hiding this comment.
public static final String ID5_ID_SOURCE = "id5-sync.com";
private BidRequestUtils() {
}
public static boolean isId5IdPresent(BidRequest bidRequest) {
return Optional.ofNullable(bidRequest.getUser())
.map(User::getEids)
.orElse(Collections.emptyList())
.stream()
.anyMatch(eid -> ID5_ID_SOURCE.equals(eid.getSource()));
}
| private static final Logger LOG = LoggerFactory.getLogger(Id5IdFetchHook.class); | ||
|
|
||
| public static final String CODE = "id5-user-id-fetch-hook"; | ||
| private final FetchClient fetchClient; |
| public Id5IdFetchHook(FetchClient fetchClient, | ||
| List<FetchActionFilter> filters, | ||
| Id5PartnerIdProvider partnerIdProvider) { | ||
| this.fetchClient = Objects.requireNonNull(fetchClient); |
| this.filters = ImmutableList.<FetchActionFilter>builder() | ||
| .addAll(Objects.requireNonNull(filters)) | ||
| .build(); |
There was a problem hiding this comment.
this.filters = Objects.requireNonNull(filters)
| private static final Logger LOG = LoggerFactory.getLogger(Id5IdInjectHook.class); | ||
|
|
||
| public static final String CODE = "id5-user-id-inject-hook"; | ||
| private final String inserter; |
| public Id5IdInjectHook(String inserter) { | ||
| this(inserter, java.util.List.of()); | ||
| } |
There was a problem hiding this comment.
remove, we don't keep code for tests
|
|
||
| public Id5IdInjectHook(String inserter, List<InjectActionFilter> filters) { | ||
| this.inserter = inserter; | ||
| this.filters = List.copyOf(Objects.requireNonNull(filters)); |
| "id5-user-id-inject: updated user with id5 eids")) | ||
| .build(); | ||
| }); | ||
| } catch (Exception e) { |
| if (filters == null || filters.isEmpty()) { | ||
| return FilterResult.accepted(); | ||
| } |
🔧 Type of changes
✨ What's the context?
ID5 wants to have its own module that can enrich bid requests with ID5 universal identifiers. This module fetches identity signals from ID5's API and automatically injects them into OpenRTB bid requests as Extended
Identifiers (EIDs).
🧠 Rationale behind the change
Why a dedicated module?
Key design decisions:
ProcessedAuctionRequestHook): Initiates async ID5 API call early in the request lifecycleBidderRequestHook): Injects EIDs into each bidder request only when not already presentTrade-offs:
🔎 New Bid Adapter Checklist
Not applicable - this is a module, not a bid adapter🧪 Test plan
Unit Tests:
Integration Tests:
sample/directoryManual Testing:
The module has been tested with various configurations and filter combinations to ensure safe production deployment.
🏎 Quality check
🤔 Questions for reviewers
1. Hook invocation status/action handling:
We would particularly appreciate review of our hook invocation status and action returns. Please verify that we're correctly returning:
InvocationStatusvalues in different scenarios (success, failure, skipped)InvocationActionto control execution flow2. Execution plan configuration simplification:
Both hooks (fetch + inject) are required for the module to function - the module is non-functional if either hook is missing from the execution plan. Currently, operators must configure both hooks separately in the
execution plan:
Question: Is there a way to simplify this configuration? For example: